浏览器脚本加载策略深度解析
在现代 Web 开发中,脚本的加载方式对页面的性能(如白屏时间、FCP 等)有着至关重要的影响。合理选择加载策略可以显著提升用户体验。
一、脚本加载时序图
下图展示了不同加载策略在 HTML 解析、脚本下载和脚本执行三个阶段的执行时序。
二、核心属性对比
1. 默认情况 (Normal Script)
- 行为: 浏览器解析 HTML 时,遇到
<script>标签会立即停止解析,开始下载脚本。下载完成后立即执行脚本,执行完毕后再恢复解析 HTML。 - 缺点: 如果脚本较大或网络较慢,会造成显著的解析阻塞,导致页面白屏。
2. async (异步)
- 代码:
<script async src="..."></script> - 下载: 异步下载,下载过程不阻塞 HTML 解析。
- 执行: 下载完立即执行。执行脚本时会暂停 HTML 解析。
- 顺序: 脚本的执行顺序不固定,谁先下载完谁先执行。
- 场景: 适用于不依赖其他脚本且不被其他脚本依赖的独立脚本(如广告、统计代码)。
3. defer (延迟)
- 代码:
<script defer src="..."></script> - 下载: 异步下载,下载过程不阻塞 HTML 解析。
- 执行: HTML 解析完成后执行(在
DOMContentLoaded事件之前)。 - 顺序: 保证执行顺序。脚本按在 HTML 中出现的顺序依次执行。
- 场景: 现代 Web 开发的首选。适用于依赖 DOM 结构或有相互依赖关系的脚本。
4. preload (预加载)
- 代码:
<link rel="preload" href="..." as="script"> - 行为: 告诉浏览器该资源是当前页面立即需要的,请以高优先级下载。
- 特点: 只下载不执行。下载后会缓存在本地,后续遇到对应的
<script>标签时直接从缓存读取。 - 场景: 用于提前加载关键资源(如核心库、关键字体、首屏 CSS)。
5. prefetch (预读取)
- 代码:
<link rel="prefetch" href="..."> - 行为: 告诉浏览器该资源可能是未来页面需要的,请在空闲时间以低优先级下载。
- 特点: 只下载不执行。
- 场景: 用于加载下一个路由或页面可能需要的资源,提升后续页面的加载速度。
三、总结对比表
| 属性 | 异步下载 | 阻塞解析 | 执行时机 | 执行顺序 | 优先级 |
|---|---|---|---|---|---|
| Normal | ✗ | ✓ | 下载后立即执行 | 顺序执行 | 高 |
| async | ✓ | 下载不阻塞,执行阻塞 | 下载完成后立即执行 | 谁快谁执行 | 高 |
| defer | ✓ | ✗ | HTML 解析完成后执行 | 顺序执行 | 高 |
| preload | ✓ | ✗ | 无(仅下载) | - | 最高 |
| prefetch | ✓ | ✗ | 无(仅下载) | - | 最低 |
四、面试高频问题
1. async 和 defer 的区别是什么?
回答:
- 相同点: 都是异步下载,下载过程都不阻塞 HTML 解析。
- 不同点:
- 执行时机:
async下载完立即执行;defer要等到 HTML 解析完成后再执行。 - 执行顺序:
async无法保证顺序(谁先下完谁执行);defer保证按照 HTML 中的出现顺序执行。 - 依赖性:
async适用于独立脚本;defer适用于需要操作 DOM 或有相互依赖关系的脚本。
- 执行时机:
2. preload 和 prefetch 的区别及使用场景?
回答:
- preload: 用于加载当前页面必须的关键资源,优先级最高。如果下载后 3 秒内未被使用,浏览器会抛出警告。
- prefetch: 用于加载未来页面可能需要的资源,优先级最低,利用浏览器空闲时间下载。
3. 为什么建议将 <script> 放在 <body> 底部?
回答: 在没有使用 async 或 defer 的情况下,脚本会阻塞 HTML 解析。放在底部可以确保 HTML 结构先被解析和渲染,避免因为脚本加载导致的页面白屏。但在使用 defer 后,脚本放在 <head> 中也能达到类似甚至更好的性能。
4. 多个 defer 脚本会按顺序执行吗?
回答: 是的。HTML 规范要求 defer 脚本必须按照它们在文档中出现的顺序执行。